Linguaggio C

 

per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto!

Riepilogo delle prime 6 lezioni

di Luca Sabatucci

Lezione 7

Pagina principale | Lezione precedente | Lezione successiva


I primi esempi completi

Sarà un fattore costante di questo corso fornire degli esempi funzionanti che l'utente può provare immediatamente sul proprio PC. Ritengo infatti che questo sia il modo migliore di reassumere i concetti già esposti. Inoltre questi programmi mostrano lo stile di programmazione su cui ho insistito molto fino ad ora, che si può reassumere brevemente in una serie di punti:

  1. Utilizzo di variabili con nomi significativi.
  2. Indentazione del codice, cioè uso degli spazi per individuare le strutture utilizzate.
  3. Commenti brevi ma chiari a destra del codice
  4. Commenti sullo scopo della funzione prima del corpo della stessa

Per coloro che non intendono digitare tali sorgenti, questi possono essere scaricati alla fine del documento.

Media di numeri interi

Questo primo esempio, molto semplice, è costituito da una sola funzione, la main, nella quale si chiede all'utente di digitare una serie di numeri interi; per concludere la serie l'utente deve digitare lo zero, quindi il programma calcola e visualizza la media dei numeri inseriti.

/* esempio 1.1                                                  */
/* programma che calcola la media della lista di numeri interi  */
/* corso di C (club di informatica: web.tiscalinet.it/clubinfo) */
/* realizzato da Luca Sabatucci (Luca.Sabatucci@infinito.it)    */

#include <stdio.h>

int main () {
    double somma = 0.0;                         /* [1] dichiarazione delle variabili */
    int numero = 0.0;
    double media;
    int contatore = 0;

    printf("Programma per calcolare la media di una lista di numeri\nInserire un intero alla volta e 0 per concludere la lista");

    do {
        printf("\n inserire un numero: ");      /* [2]  richiesta di input*/
        scanf("%d",&numero);

        somma += numero;                        /* [3]  istruzioni interne al ciclo */
        if (numero != 0)
            contatore++;

    } while (numero != 0);                      /* [4]  condizione di uscita dal ciclo */

    if (contatore != 0)                         /* [5]  calcolo della media */
        media = somma / (double) contatore;

    printf("\n La media e' %g",media);          /* [6]  visualizazione del risultato */

    return 0;                                   /* [7]  fine del programma */
}

/* per prima cosa si noti che i commenti si mettono tra i simboli sbarra e asterisco
  (per aprire) e asterisco sbarra per chiudere. Il commento può andare a capo e non
  possono essere messi commenti dentro altri commenti */

/* [1]: dichiarazione delle variabili.
    somma - contiene la somma di tutti i numeri inseriti
    numero - contiene il numero digitato dall'utente
    media - serve per il calcolo finale della media
    contatore - serve per determinare quandi numeri sono stati inseriti

   [2]: richiesta di input.
    Poiche' non abbiamo ancora studiato le funzioni di i/o tralasciamo queste
    due funzioni. Basti sapere che richiedono all'utente di digitare un numero

   [3]: istruzioni del ciclo.
    Il numero inserito dall'utente viene sommato agli altri. Se il numero inserito non
    e' zero allora viene incrementato il contatore dei numeri inseriti.
    Per chi non ricordi la formula per calcolare la media, questa è:
        sommatoria(numeri) / numero di numeri

   [4]: condizione di uscita dal ciclo.
     Il ciclo viene ripetuto finche' la variabile numero non contiene zero

   [5]: solo nel caso in cui sia stato inserito almeno un numero si calcola la media
    dividendo la sommatoria già calcolata per il numero di valori inseriti

   [6]: si visualizza il risultato su schermo.

   [7]: si esce dal programma */

Si noti come l'uso di commenti in questo listato sia esteso, addirittura eccessivo. Quando si assume una certa familiarità del linguaggio, commentare così tanto diviene controproducente. Ad esempio il commento [2] non è necessario. Un programmatore alle prime armi sa già che quelle istruzioni richiedono un input all'utente.
Nel commento [1] invece si spiega il significato di ogni variabile. anche questo può essere evitato se il programmatore assume uno "stile", ovvero associa ad una variabile sempre lo stesso scopo. Ad esempio cont viene usato sempre per contare in numero di iterazioni di un ciclo. In questo caso non è necessario commentare ogni volta la variabile, ma si può fare una volta per tutte all'inizio del listato, o in un file di testo indipendente.

Numeri dispari

Adesso introduciamo un livello di difficoltà successivo (niente di sconvolgente): una funzione. In questo esempio vogliamo stampare a schermo i numeri dispari inferiori ad una costante. Il controllo se un numero è dispari viene svolto da un'apposita funzione.

/* esempio 1.2                                                  */
/* programma che stampa i numeri dispari minori di N            */
/* corso di C (club di informatica: web.tiscalinet.it/clubinfo) */
/* realizzato da Luca Sabatucci (Luca.Sabatucci@infinito.it)    */

#include <stdio.h>
#define N  20

int dispari(int numero);


int main() {
    int count;

    for (count=0; count < N; count++)	/* effettua N iterazioni */
        if (dispari(count))
            printf("%d\n",count);	/* se il numero è dispari lo stampa */

    return 0;
}


/* funzione che determina se un numero è dispari */
/* restituisce  0 se il numero è pari */
/*              1 se il numero è dispari */
int dispari(int numero) {
    int resto;				/* variabile usata per calcolare il resto della divisione */

    resto = numero % 2;

    if (resto >= 1)
        return 1;
    else
        return 0;
}

La prima cosa che si nota è l'uso di una direttiva che non abbiamo ancora incontrato: #define. Come suggerisce il nome si tratta di un modo per definire una costante. Questa costante è diversa da quella definita con la parola chiave const: non occupa spazio di memoria; questo poichè #define è una direttiva di compilazione. Significa che prima che il programma venga eseguito tutte le occorrenze di N vengono sostituite con il valore assegnato, nel nostro caso 20.
Si noti che questa volta la variabile count non è commentata: sppiamo già qual'è il suo scopo.
All'interno del ciclo, count assume i valori dei numeri interi compresi tra 0 e N. La funzione dispari determina se il numero è pari o dispari. Solo nel caso in cui il numero è dispari, viene passato alla funzione printf che lo stampa su schermo.

Il commento sulla funzione dispari già spiega abbastanza chiaramente cosa fa tale funzione. Se anche non conoscessimo il contenuto della funzione saremmo in grado di usarla correttamente. E' molto importante, quando una funzione restituisce un valore, spiegare i significati di tale valore. Nel nostro caso è stato molto semplice perchè sono contemplati due soli possibili stati: dispari / pari.
La funzione in se sfrutta una proprità dei numeri dipari per determinare quando un numero è pari o dispari. Un numero dispari, non è divisibile per 2, cioè nella divisione per 2 dà un resto. Quindi non ci resta che effettuare la divisione, calcolare il resto (operazione già effettuata dall'operatore % modulo) e valutare se questo è nullo (numero pari) o maggiore di zero (numero dispari).

Numeri primi

Quello della determinazione dei numeri primi è un annoso problema che ancora non ha trovato una soluzione efficiente. Delle soluzioni chiaramente esistono, ma non sono efficienti, cioè consumano troppa memoria o impiegano troppo tempo. Potremmo affrontare il problema dell'efficienza, ma questo ci porterebbe a scrivere una lezione solo su questo argomento. Quindi basti sapere che esistono dei problemi chiamati "difficili" proprio perchè la soluzione viene trovata o con un eccessivo impiego di tempo o di memoria. Quello della determinazione dei numeri primi è un problema "difficile".

Qui riporto una possibile soluzione, la quale è efficiente dal punto di vista dello spazio di memoria usato, ma è inefficiente per il tempo impiegato.
Ricordiamo la definizione: un numero si dice primo se è divisibile solo per 1 e per se stesso; sono numero primi 1, 2, 3, 5, 7... e così via.
In questo programmino sfruttiamo proprio la definizione.

#include 

#define N 50
#define TRUE  1
#define FALSE 0

int numero_primo(int n);
int divisibile(int numero, int divisore);

int main(int argc, char* argv[]) {
        int count;

        for (count=1; count < N; count++)		/* ripete la cosa per i primi 50 numeri interi */
            if (numero_primo(count))
                printf("%d\n",count);


	return 0;
}

/* la funzione determina se un numero e' un numero primo, cioe' 
   se e' divisibile solo per 1 e per se stesso /*
/* restituisce TRUE se e' un numero primo /*
/*             FALSE se non e' un numero primo */
int numero_primo(int n) {
    int count;
    int primo=TRUE;								/* permette di uscire dal ciclo se il numero
						           				   non è primo */
    
    if (n!=1) {									/* il caso n == 1 è considerato speciale */

        count=2;
        while (primo == TRUE && count < n) {	/* divide il numero per tutti i suoi precedessori */
            primo = !divisibile(n,count);		/* finche' non incontra un numero */
            count++;							/* per cui e' divisibile */
        }

    }

    return primo;
}

/* la funzione determina se un numero e' divisibile per un altro /*
/* restuisce TRUE se il numero e' divisibile */
/*           FALSE se non e' divisibile */
int divisibile(int numero, int divisore) {
    int resto;

    resto = numero % divisore;

    if (resto <= 1)
        return FALSE;
    else
        return TRUE;
}

Il problema è affrontato in modo del tutto simile al precedente. Qui però abbiamo tre funzioni anzichè due. Nella main facciamo le iterazioni dei numeri naturali fino a N e per ogni numero controlliamo se è un numero primo o no.
Nelle definizioni iniziali inseriamo anche due costanti molto comode TRUE e FALSE che ci permettono di lavorare con i valori booleani.
La funzione chiave è numero_primo che controlla se il numero passato come argomento è primo. Per determinarlo si inizia un ciclo nel quale si verifica se l'argomento è divisibile per tutti i suoi predecessori a partire da 2 in poi. Per essere un numero primo non deve essere divisibile per nessuno di essi. Ci è convenuto usare un ciclo while, piuttosto che for, perchè qualora troviamo che l'agomento è divisibile per un suo predecessore possiamo subito concludere che non è un numero primo ed escire dal ciclo. Se invece l'argomento non è divisibile per nessuno dei suoi predecessori, allora si esce dal ciclo grazie alla condizione count<n.

La funzione divisibile determina in modo analogo alla funzione dispari se un numero è divisibile per un altro, cioè valutando il resto della divisione.

Compilazione

Adesso che abbiamo a disposizione qualche programmino pronto e funzionante, sicuramente ai più è venuta voglia di provare a eseguirli. Vediamo come è possibile farlo: niente paura, non si tratta di un operazione particolarmente complessa.
Per prima cosa dobbiamo avere a disposizione un compilatore.

Il compilatore

Se già vi siete procurati il compilatore con cui intendete eseguire questi programmi non avete bisogno di leggere questo paragrafo. Se invece desiderate avere un suggerimento su quale compilatore procurarvi continuate a leggere.

Per prima cosa chiariamo che spesso quello che si indica come compilatore è in realtà una collezione di programmi e di librerie e non una sola applicazione. In passato questi programmi venivano eseguiti direttamente e indipendentemente dall'utente mediante riga di comando (dos o unix); adesso i tempi sono cambiati completamente; i progettisti di compilatori sono venuti incontro alle esigenze di un programmatore "dilettante" che non ha alcuna voglia di imparare ad usare la sintassi di comandi spesso ostici. Sono stati sviluppati degli "ambienti integrati", che permettono in un unico ambiente di editare il listato e di compilarlo mediante uso di interfaccia grafica a finestre a cui siamo ormai tanto abituati.

In commercio esistono una gran quantità di compilatori per il linguaggio C, ma non sono da disprezzare (soprattutto per i nostri scopi didattici) neppure le versioni freeware o shareware, spesso di ottima qualità.

C'è anche da notare che i compilatori in commercio raramente, oramai, sono solo compilatori C. Molto più spesso sono compilatori C++ (un linguaggio che deriva dal C, ma è orientato agli oggetti), e magari forniscono anche estensioni visuali per i sistemi operativi più comuni (finestre, bottoni, list box...). Non c'è da preoccuparsi se si ha a disposizione un compilatore simile, infatti tutti i compilatori C++ riconoscono e compilano anche i "vecchi" programmi C, e le estensioni visuali è quasi sempre possibile disattivarle. Conviene consultare il manuale per le spiegazioni.
Alcuni esempi di compilatori commerciali sono:

  1. Microsoft Quick C (DOS/Windows)
  2. Borland Turbo C (DOS/Windows)
  3. Metrowerks Codewarrior (Macintosh)
  4. Think C (Macintosh)

Alcuni compilatori non commerciali per DOS/Windows sono:

  1. Cygnus gcc
  2. GNU compiler

Per l'ambiente Unix invece il compilatore C è fornito direttamente insieme al sistema operativo; faremo alcuni esperimenti anche per questo ambiente.

Il processo di compilazione

Supponiamo per semplicità di avere a disposizione un compilatore C con un ambiente integrato. In genere questi ambienti forniscono la possibilità di eseguire, tra l'altro, le operazioni di "Build" e di "Run".
Digitiamo nella finestra di edit il nostro listato. A questo punto l'operazione da compiere è di trasformare il programma in una applicazione eseguibile in modo indipendente.
Questa operazione è svolta dal comando "Build" che letteralmente costruisce l'applicazione finale. Il comando "Run" in più esegue il programma in una modalità particolare in cui è possibile controllare le singole istruzioni, ed effettuare l'analisi del corretto funzionamento (fase di debug).

Analizziamo però in dettaglio il processo che porta dal listato ad una applicazione eseguibile, così da poter apprezzare ancora di più la potenza di un ambiente integrato.

Prima che il compilatore inizi a lavorare un modulo, chiamato preprocessore, si occupa di interpretare alcune direttive di pre-compilazione. Alcune le abbiamo già viste (#include, #define) altre le vedremo (#if, #endif), altre sono specifiche del compilatore usato (ad esempio #uses). Mediante le specifiche di precompilazione diamo alcune informazioni al compilatore come deve comportarsi in determinate situazioni. E' interessante ad esempio la direttiva che permette di fare compilare a compilatori diversi parti diverse del listato; situazioni simili si avevano molto spesso quando si usavano i due compilatori concorrenti Turbo C e Quick C. Poichè alcune funzioni avanzate (ad esempio quelle per la grafica) erano supportate da entrambi, ma con nomi differenti, si usavano queste direttive per rendere il listato compatibile ad entrambi i compilatori.

Il compilatore quindi trasforma il listato in un file oggetto, ovvero in un insieme di istruzioni in linguaggio macchina, che ancora non può essere eseguito così per come è. Capita molto spesso, per i programmi piuttosto lunghi, di spezzarli in più file; questi vengono compilati separatamente.

La fase successiva è quella del linker; questo effettua l'operazione fondamentale di riunire in un unico file eseguibile i vari file oggetto del listato e delle librerie. Infatti ogni volta che si utilizza una funzione contenuta in una libreria (ad esempio la printf) il codice di questa deve essere incluso nel file finale. Il linker inoltre aggiunge una porzione di codice chiamato loader; questa è indispensabile a eseguire il programma sul sistema operativo specifico.

Tutte queste operazioni, che qualche tempo fa erano eseguite dall'utente, una per una, adesso sono automatizzate da un unico processo che parte quando si preme sul pulsante "Build" dell'ambiente integrato. Non che eseguire a mano tutte le operazioni fosse troppo complesso, ma sicuramente poteva risultare noiso, e faceva perdere maggiore tempo.


Listati presenti in questa lezione

  1. Media di numeri interi
  2. Numeri dispari
  3. Numeri primi

Bibliografia


Testi consigliati per l'apprendimento

Questo articolo è stato scaricato dal Club di informatica
Pagina curata da Luca Sabatucci